<?php
#
# dmBridge: a data access framework for CONTENTdm(R)
#
# Copyright © 2009, 2010, 2011 Board of Regents of the Nevada System of Higher
# Education, on behalf of the University of Nevada, Las Vegas
#

/**
 * Models a real-world CONTENTdm(R) object (item) - not a PHP object. DMObjects
 * should be instantiated using <code>DMObjectFactory::getObject()</code>.
 * This will ensure that no duplicate objects are instantiated.
 *
 * @author Alex Dolski <alex.dolski@unlv.edu>
 * @license http://www.opensource.org/licenses/mit-license.php
 */
class DMObject extends DMModel implements DMURIAddressable {

	/** @var The object's XML representation from CONTENTdm(R) */
	private $cdm_domdocument;
	/** @var array Array of DMObjects (if a compound object); lazy-loaded by
		getChildren() */
	private $children = array();
	/** @var DMCollection */
	private $collection;
	/** @var "Document" or "Document-PDF" depending on compound object type.
	 * Affects how PDF URLs are generated. */
	private $compound_type;
	/** @var DMConfigIni */
	private $config;
	/** @var DMDataStore */
	private $data_store;
	/** @var DMDateTime */
	private $date_created;
	/** @var DMDateTime */
	private $date_updated;
	/** @var DMFile */
	private $file;
	/** @var string */
	private $filename;
	/** @var int */
	private $image_height;
	/** @var int */
	private $image_width;
	/** @var array Array of DMDCElement objects */
	private $metadata = array();
	/** @var int */
	private $page;
	/** @var DMObject */
	private $parent_obj;
	/** @var int */
	private $ptr;
	/** @var DMFile */
	private $thumbnail_file;
	/** @var string */
	private $title;
	/** @var DMObjectViewerDelegate */
	private $viewer;

	/**
	 * @param DMCollection col
	 * @param ptr
	 * @return boolean
	 */
	public static function exists(DMCollection $col, $ptr) {
		$xml = null;
		dmGetItemInfo($col->getAlias(), $ptr, $xml);
		return (bool) $xml;
	}

	/**
	 * Not for public use; to instantiate a DMObject, use
	 * <code>DMObjectFactory::getObject()</code> instead.
	 *
	 * @param DMCollection collection
	 * @param int ptr
	 * @param DMDataStore data_store
	 * @param DMConfigIni config
	 */
	public function __construct(DMCollection $collection, $ptr,
			DMDataStore $data_store, DMConfigIni $config) {
		$this->collection = $collection;
		$this->ptr = abs($ptr);
		$this->data_store = $data_store;
		$this->config = $config;
	}

	/**
	 * @return string The DC title of the DMObject (if available), or "alias
	 * pointer" if not.
	 * @since 0.1
	 */
	public function __toString() {
		try {
			$value = ($this->getMetadata("title") instanceof DMDCElement)
				? $this->getMetadata("title")->getValue()
				: $this->getCollection()->getAlias() . " " . $this->getPtr();
			return $value;
		} catch (Exception $e) {
			return $this->getCollection()->getAlias() . " " . $this->getPtr();
		}
	}

	/**
	 * Retrieves the URI of the DMObject's item record in CONTENTdm(R). Use of
	 * the object's reference URL (accessible by
	 * <code>getReferenceURL()</code>) instead is preferred.
	 *
	 * @return string
	 * @see getReferenceURL()
	 */
	public function getCdmURI() {
		$uri = clone DMHTTPRequest::getCurrent()->getURI()->getAbsoluteHostURI();
		if ($this->isCompound()) {
			$uri->setPath("/cdm4/document.php");
			$uri->addQueryValue("CISOROOT", $this->getCollection()->getAlias());
			$uri->addQueryValue("CISOPTR", $this->getPtr());
		} else if ($this->isChild()) {
			$uri->setPath("/cdm4/document.php");
			$uri->addQueryValue("CISOROOT", $this->getCollection()->getAlias());
			$uri->addQueryValue("CISOPTR", $this->getParent()->getPtr());
			$uri->addQueryValue("CISOSHOW", $this->getPtr());
		} else {
			$uri->setPath("/cdm4/item_viewer.php");
			$uri->addQueryValue("CISOROOT", $this->getCollection()->getAlias());
			$uri->addQueryValue("CISOPTR", $this->getPtr());
		}
		return $uri;
	}

	/**
	 * Returns the child object at the given index (equivalent to page minus
	 * 1), or null if none exists at that index.
	 *
	 * @param int index
	 * @return DMObject|null
	 * @see getChildren()
	 */
	public function getChild($index) {
		$children = $this->getChildren();
		if (!array_key_exists($index, $children)) {
			return null;
		}
		return $children[$index];
	}

	/**
	 * @return Boolean
	 */
	public function isChild() {
		return ($this->getParent() instanceof DMObject);
	}

	/**
	 * @return Array of all of the DMObject's child DMObjects in page order,
	 * or an empty array if none exist or if the DMObject is not compound.
	 *
	 * @see getChild()
	 */
	public function getChildren() {
		if (count($this->children)) {
			return $this->children;
		}

		// get compound object structure
		$xml = null;
		dmGetCompoundObjectInfo(
			$this->getCollection()->getAlias(), $this->getPtr(), $xml);
		if (!strlen($xml)) {
			return array();
		}

		$dxml = new DOMDocument("1.0", "utf-8");
		$dxml->loadXML($xml);

		// instantiate children
		foreach ($dxml->getElementsByTagName("pageptr") as $page) {
			$this->children[] = DMObjectFactory::getObject(
				$this->getCollection(), (int) $page->nodeValue);
		}
		return $this->children;
	}

	/**
	 * Returns true if the object is compound and has children, or false if it
	 * is either not compound, or has no children.
	 *
	 * @return Boolean
	 * @see isCompound()
	 */
	public function hasChildren() {
		return (bool) count($this->getChildren());
	}

	/**
	 * @return DMCollection The object's parent collection
	 */
	public function getCollection() {
		return $this->collection;
	}

	/**
	 * Convenience method that appends a new comment to the DMObject.
	 *
	 * @param DMComment comment
	 * @return True if the comment was successfully saved; false if not.
	 * @throws DMDataStoreException
	 * @throws DMPDOException
	 */
	public function addComment(DMComment $comment) {
		return $this->data_store->addObjectComment($this, $comment);
	}

	/**
	 * @return Boolean True if the object has at least one approved comment, or
	 * false if not
	 */
	public function hasComments() {
		return (count($this->data_store
				->getApprovedCommentsForObject($this, 1, 1000)) > 0);
	}

	/**
	 * Retrieves the URI of the feed for the list of object comments.
	 *
	 * @return string
	 * @since 1.1
	 */
	public function getCommentsFeedURL() {
		$params = "objects" . $this->getCollection()->getAlias()
			. "/" . $this->getPtr() . "/comments";
		return DMInternalURI::getURIWithParams($params, array(), "atom");
	}

	/**
	 * Whether the object is compound, even if it has no pages.
	 *
	 * @return Boolean
	 */
	public function isCompound() {
		if (count($this->children)) {
			return true;
		}

		// get compound object structure
		$xml = "";
		dmGetCompoundObjectInfo(
			$this->getCollection()->getAlias(), $this->getPtr(), $xml);
		return (bool) strlen($xml);
	}

	/**
	 * Retrieves the DMObject's "dmcreated" value from CONTENTdm(R).
	 *
	 * @return DMDateTime
	 */
	public function getDateCreated() {
		if (!$this->date_created instanceof DMDateTime) {
			$dxml = $this->getCdmDOMDocument();
			$created = $dxml->getElementsByTagName("dmcreated")->item(0)->nodeValue;
			$this->date_created = new DMDateTime($created);
		}
		return $this->date_created;
	}

	/**
	 * Retrieves the DMObject's "dmmodified" value from CONTENTdm(R).
	 *
	 * @return DMDateTime
	 */
	public function getDateUpdated() {
		if (!$this->date_updated instanceof DMDateTime) {
			$dxml = $this->getCdmDOMDocument();
			$updated = $dxml->getElementsByTagName('dmmodified')->item(0)->nodeValue;
			$this->setDateUpdated(new DMDateTime($updated));
		}
		return $this->date_updated;
	}

	/**
	 * @param DMDateTime date
	 */
	protected function setDateUpdated(DMDateTime $date) {
		$this->date_updated = $date;
	}

	/**
	 * @param mixed obj
	 * @return True if the object to be compared has the same collection alias
	 * and pointer as the instance; false if not.
	 * @since 2.0
	 */
	public function equals($obj) {
		return (is_object($obj) && $obj instanceof DMObject
				&& $this->getCollection()->getAlias()
				== $obj->getCollection()->getAlias()
				&& $this->getPtr() == $obj->getPtr());
	}

	/**
	 * Checks whether the DMObject is in the user's favorites.
	 *
	 * @return Boolean
	 * @since 0.1
	 */
	public function isFavorite() {
		$session = DMHTTPRequest::getCurrent()->getSession();
		foreach ($session->getAllFavorites() as $f) {
			if ((string) $f == (string) $this) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Convenience method equivalent to calling
	 * <code>getURI(DMBridgeComponent::TemplateEngine, "atom")</code>.
	 *
	 * @return DMInternalURI
	 */
	public function getFeedURI() {
		return $this->getURI(DMBridgeComponent::TemplateEngine, "atom");
	}

	/**
	 * @param string nick
	 * @return DMDCElement If $nick is a valid field nickname, returns a
	 * DMDCElement object corresponding to the field. If it is not, returns
	 * null.
	 */
	public function getField($nick) {
		return $this->getMetadata($nick);
	}

	/**
	 * Adds a metadata element, or replaces an existing element with the same
	 * nick. This will not be persisted.
	 *
	 * @param DMDCElement elem
	 */
	public function addField(DMDCElement $elem) {
		$count = count($this->metadata);
		for ($i = 0; $i < $count; $i++) {
			if ($this->metadata[$i]->getNick() == $elem->getNick()) {
				$this->metadata[$i] = $elem;
				return;
			}
		}
		$this->metadata[] = $elem;
	}

	/**
	 * Alias of getMetadata().
	 *
	 * @return array An array of all of the DMObject's fields as DMDCElement
	 * objects.
	 * @see getMetadata()
	 */
	public function getFields() {
		return $this->getMetadata();
	}

	/**
	 * Returns a DMFile pointing to the object's file on disk. Does not check
	 * whether the file actually exists. Note that direct filesystem access
	 * to CONTENTdm(R) files is not supported by OCLC, so use at your own risk.
	 * For certain media types, a subclass of DMFile may be returned, such as
	 * DMPDFFile.
	 * 
	 * @return DMFile
	 * @see getFilename()
	 */
	public function getFile() {
		if (!$this->file) {
			$arg = $this->config->getString("contentdm.collections.pathname")
				. $this->getCollection()->getAlias() . "/image/"
				. $this->getFilename();
			switch ((string) $this->getMediaType()) {
				case "application/pdf":
					$this->file = new DMPDFFile($arg);
					break;
				default:
					$this->file = new DMFile($arg);
					break;
			}
		}
		return $this->file;
	}

	/**
	 * @return string The filename of the DMObject, without any path
	 * information.
	 * @see getFile()
	 * @deprecated Deprecated as of version 2.1. Use getFile() instead.
	 */
	public function getFilename() {
		if (strlen($this->filename) < 1) {
			$this->instantiateFileProperties();
		}
		return basename($this->filename);
	}

	/**
	 * @param string filename
	 * @since 0.1
	 */
	protected function setFilename($filename) {
		$this->filename = $filename;
	}

	/**
	 * Returns the public URL of the object's file.
	 *
	 * @param boolean resolve_links Whether to resolve the redirect URLs of
	 * URL-type objects. This relates to an implementation detail of
	 * CONTENTdm(R) and should normally be true. For non-URL-type objects, it
	 * will have no effect.
	 * @return DMURI, or null if $resolve_links is true and no destination URL
	 * can be discerned.
	 */
	public function getFileURL($resolve_links = true) {
		if ($this->isURLType() && $resolve_links) {
			$uri = new DMURI($this->getFileURL(false));
			$req = new DMHTTPRequest($uri);
			$htc = new DMHTTPClient($req);
			$response = $htc->send();
			$body = $response->getRepresentation()->getBody();
			$tmp = explode("URL=", $body);
			return (count($tmp) > 1) ? trim($tmp[1]) : null;
		} else if ($this->getCompoundType() == "Document-PDF") {
			$qa = array(
				'CISOROOT' => $this->getCollection()->getAlias(),
				'CISOPTR' => ($this->isChild())
					? $this->getParent()->getPtr() : $this->getPtr(),
				'CISOPAGE' => $this->getPage()
			);
			$base_uri = rtrim(DMHTTPRequest::getCurrent()->getURI()->getAbsoluteHostURI(), "/")
				. $this->config->getString("contentdm.showpdf_exe.path");
			return new DMURI(
				rtrim($base_uri, "/") . "?" . urldecode(http_build_query($qa)));
		}
		return DMInternalURI::getURIWithParams("api/1/objects"
			. $this->getCollection()->getAlias() . "/" . $this->getPtr()
			. "/bitstream");
	}

	/**
	 * Returns the DMDCElement object corresponding to the
	 * object's fulltext field (with nick &quot;full&quot;). If the DMObject is
	 * a compound object, returns the fulltext field corresponding to the first
	 * page. To avoid this behavior, use getMetadata("full").
	 *
	 * @return DMDCElement object, or false if not available
	 * @see getMetadata()
	 * @since 0.1
	 */
	public function getFullText() {
		/* Check for the presence of a non-empty "full" field. The object itself
		may have one; or, it may be a compound object, one or more of whose
		children would have one. We need to check both cases. */
		$text = $this->getMetadata("full");
		// Does the object itself have a non-empty fulltext field?
		if (!$text instanceof DMDCElement || strlen($text->getValue()) < 1) {
			// no, so check children, if applicable
			if ($this->hasChildren()) {
				$text = $this->getChild(0)->getMetadata("full");
			}
		}
		return $text;
	}

	/**
	 * @return The full height of the DMObject's image, if applicable.
	 * @deprecated Deprecated in version 2.1. Use getImageHeight() instead.
	 */
	public function getHeight() {
		return $this->getImageHeight();
	}

	/**
	 * @param int maxwidth
	 * @param int maxheight
	 * @param int x X offset
	 * @param int y Y offset
	 * @param int rotate
	 * @return string Absolute URL of the DMObject's image with
	 * dimensions and orientation corresponding to the supplied parameters, or
	 * false if the height/width of the image are 0 (such as would be the case
	 * if it did not have one).
	*/
	public function getImageURL($maxwidth, $maxheight, $x = 0, $y = 0,
			$rotate = 0) {
		// check width & height
		// cdm defaults to 500 for each, cropped from x & y, not scaled.
		if ($maxwidth <= 0) $maxwidth = 500;
		if ($maxheight <= 0) $maxheight = 500;

		// check x & y
		if ($x <= 0) $x = 0;
		if ($y <= 0) $y = 0;

		// check rotation
		if ($rotate <= -360 || $rotate >= 360) $rotate = null;

		// calculate display scale
		$h = $this->getImageHeight();
		$w = $this->getImageWidth();
		if (!$h || !$w) {
			return false;
		}
		$hscale = ($maxheight / $h) * 100;
		$wscale = ($maxwidth / $w) * 100;
		$scale = ($hscale < $wscale) ? $hscale : $wscale;

		// set up query array for http_build_query()
		$qs = array(
			'CISOROOT' => $this->getCollection()->getAlias(),
			'CISOPTR' => $this->getPtr(),
			'DMSCALE' => round($scale, 6),
			'DMWIDTH' => $maxwidth,
			'DMHEIGHT' => $maxheight,
			'DMX' =>	$x,
			'DMY' => $y,
			'DMROTATE' => $rotate
		);

		return new DMURI(rtrim(DMHTTPRequest::getCurrent()->getURI()->getAbsoluteHostURI(), "/")
			. $this->config->getString("contentdm.getimage_exe.path")
			. "?" . urldecode(http_build_query($qs)));
	}

	/**
	 * @return int The full height of the DMObject's image, if applicable.
	 * @since 2.1
	 */
	public function getImageHeight() {
		if ($this->image_height < 1) {
			$this->instantiateFileProperties();
		}
		return $this->image_height;
	}

	/**
	 * @return int The full width of the DMObject's image, if applicable.
	 * @since 2.1
	 */
	public function getImageWidth() {
		if ($this->image_width < 1) {
			$this->instantiateFileProperties();
		}
		return $this->image_width;
	}

	/**
	 * @param string nick
	 * @return If <code>$nick</code> is supplied and a valid field nickname,
	 * returns a DMDCElement object corresponding to the field. If it is
	 * supplied but not valid, returns null. If it is not supplied, returns an
	 * array of all of the DMObject's fields, as DMDCElement objects.
	 * @see <code>getField()</code>
	 */
	public function getMetadata($nick = null) {
		if (count($this->metadata) < 1) {
			$this->instantiateFields();
		}
		if (!is_null($nick)) {
			foreach ($this->metadata as $field) {
				if ($field->getNick() == $nick) {
					return $field;
				}
			}
			return null;
		}
		return $this->metadata;
	}

	/**
	 * Returns a best guess at the file's media (MIME) type. The guess is
	 * based exclusively on the file's filename extension, as the CONTENTdm(R)
	 * PHP API does not supply media type information. If the media type
	 * cannot be guessed, returns a media type of "unknown/unknown".
	 *
	 * @return DMMediaType
	 * @since 2.0
	 */
	public function getMediaType() {
		$default_type = new DMMediaType("unknown", "unknown");

		// extract filename extension
		$tmp = explode(".", $this->getFilename());
		if (count($tmp) < 2) {
			// no extension, abort
			return $default_type;
		}

		// If the object is compound, it will have an extension of ".cpd,"
		// which isn't very useful. So use the extension of its first child
		// instead.
		if ($this->isCompound()) {
			return $this->getChild(0)->getMediaType();
		}

		$ext = strtolower($tmp[count($tmp)-1]);
		if ($ext == "cpd") {
			if ($this->getCompoundType() == "Document-PDF") {
				$ext = "pdf";
			}
		}

		$type = DMMediaType::getTypeForExtension($ext);
		if ($type) {
			return $type;
		}
		return $default_type;
	}

	/**
	 * @return int
	 */
	public function getPage() {
		if ($this->page > 0) {
			return $this->page;
		}
		$this->setChildProperties();
		return $this->page;
	}

	/**
	 * @return DMObject, or null if no parent exists
	 */
	public function getParent() {
		if ($this->parent_obj instanceof DMObject) {
			return $this->parent_obj;
		}
		dmGetCollectionParameters($this->getCollection()->getAlias(), $name,
				$path);
		$result = GetParent($this->getCollection()->getAlias(), $this->getPtr(),
				$path);
		if ($result > -1) {
			$this->parent_obj = DMObjectFactory::getObject(
				$this->getCollection(), $result);
			return $this->parent_obj;
		}
		return null;
	}

	/**
	 * @return int
	 */
	public function getPtr() {
		return $this->ptr;
	}

	/**
	 * Appends a new rating to the instance and saves it to the data store.
	 *
	 * @param DMRating r
	 * @return void
	 * @throws DMDataStoreException
	 */
	public function addRating(DMRating $r) {
		$this->data_store->addObjectRating($this, $r);
	}

	/**
	 * Retrieves the DMObject's rating from the data store, as a float.
	 *
	 * @param int out_of
	 * @param int decimal_places
	 * @return float
	 * @throws DMDataStoreException
	 */
	public function getRating($out_of = 100, $decimal_places = 10) {
		if ($out_of <> round($out_of) || $out_of < 1) {
			return false;
		}
		$rating = $this->data_store->getRatingForObject($this);
		$rating = ($rating / 100) * $out_of;
		return (float) round($rating, $decimal_places);
	}

	/**
	 * Retrieves the DMObject's absolute reference URL.
	 *
	 * @return string
	 * @see getCdmURI()
	 * @see getURI()
	 */
	public function getReferenceURL() {
		return rtrim(DMHTTPRequest::getCurrent()->getURI()->getAbsoluteHostURI(), '/') . '/u?'
			. $this->getCollection()->getAlias() . ',' . $this->getPtr();
	}

	/**
	 * Convenience method that appends a new tag to the instance and saves
	 * it to the data store.
	 *
	 * @param DMTag tag
	 * @return void
	 * @throws DMDataStoreException
	 */
	public function addTag(DMTag $tag) {
		$this->data_store->addObjectTag($this, $tag);
	}

	/**
	 * Returns a DMFile pointing to the object's thumbnail image on disk. Does
	 * not check whether the file actually exists. Note that direct filesystem
	 * access to CONTENTdm(R) files is not supported by OCLC, so use at your
	 * own risk.
	 * 
	 * @return DMFile
	 */
	public function getThumbnail() {
		if (!$this->thumbnail_file) {
			// Temporary hack to work with dmulator's /dynamic collection.
			// Hope nobody has a collection called /dynamic...
			if ($this->getCollection()->getAlias() == "/dynamic") {
				$this->thumbnail_file = new DMFile(realpath(
					$this->config->getString("contentdm.collections.pathname")
					. $this->getCollection()->getAlias() . "/dynamic_thumb.jpg"));
			} else {
				$this->thumbnail_file = new DMFile(realpath(
					$this->config->getString("contentdm.collections.pathname")
					. $this->getCollection()->getAlias() . "/image/icon"
					. $this->getFile()->getNameWithoutExtension() . ".jpg"));
			}
		}
		return $this->thumbnail_file;
	}

	/**
	 * Wraps getThumbnailURL().
	 *
	 * @return string
	 * @deprecated Deprecated as of version 2.1. Use getThumbnailURL() instead.
	 */
	public function getThumbURL() {
		return $this->getThumbnailURL();
	}

	/**
	 * Returns the URI of the DMObject's thumbnail image.
	 *
	 * @return DMURI
	 */
	public function getThumbnailURL() {
		$qs = array(
			'CISOROOT' => $this->getCollection()->getAlias(),
			'CISOPTR' => $this->getPtr()
		);
		return new DMURI(rtrim(DMHTTPRequest::getCurrent()->getURI()->getAbsoluteHostURI(), "/")
			. $this->config->getString("contentdm.thumbnail_exe.path")
			. "?" . urldecode(http_build_query($qs)));
	}

	/**
	 * Returns the DMObject's page title, which may be different from the
	 * metadata title returned by DMObject::getField() or getMetadata().
	 *
	 * @see getField()
	 * @see getMetadata()
	 */
	public function getTitle() {
		if (strlen($this->title) > 0) {
			return $this->title;
		}
		$this->setChildProperties();
		return $this->title;
	}


	/**
	 * @param int component One of the constants in DMBridgeComponent.
	 * @param string representation An optional alternate representation such
	 * as "atom". The default is HTML.
	 * @param boolean resolve_links If true, returns the URL of a "URL
	 * object." If false, returns its physical URI in dmBridge. Affects only
	 * objects that were created as "URL"-type objects in CONTENTdm(R).
	 * Supplying false for this parameter is not recommended in the template
	 * engine because it will link to an object view page that might have
	 * otherwise never been seen.
	 * @return DMURI The absolute URI of the instance within the given
	 * component and with the given representation.
	 * @since 2.0
	 */
	public function getURI($component = DMBridgeComponent::TemplateEngine,
			$representation = null,
			$resolve_links = true) {
		if ($resolve_links) {
			// if it's a ".url" object, it will link elsewhere
			if ($this->isURLType()) {
				return $this->getFileURL();
			}
		}
		if ($component == DMBridgeComponent::TemplateEngine
				&& $representation == null) {
			$representation = "html";
		} else if ($component == DMBridgeComponent::HTTPAPI
				&& $representation == null) {
			$representation = DMHTTPRequest::getCurrent()->getUri()->getExtension();
		}
		return DMInternalURI::getResourceURI(
				$component,
				"objects" . $this->getCollection()->getAlias()
					. "/" . $this->getPtr(),
				$representation);
	}

	/**
	 * @return boolean Whether the instance is a "URL-type" object.
	 */
	public function isURLType() {
		$tmp = explode(".", $this->getFilename());
		return (count($tmp) && $tmp[count($tmp) - 1] == "url");
	}

	/**
	 * @param int width_override
	 * @param int height_override
	 * @return DMObjectViewerDelegate Will return null if there is no viewer
	 * definition for the object's media type.
	 * @throws DMClassNotFoundException If a viewer definition is set but the
	 * viewer class cannot be found.
	 */
	public function getViewer($width_override = null, $height_override = null) {
		if (!$this->viewer) {
			$def = $this->getCollection()->getViewerDefinitionForMediaType(
					$this->getMediaType());
			if (!$def) {
				$def = $this->getCollection()->getViewerDefinitionForMediaType(
						new DMMediaType("unknown", "unknown"));
			}
			if (!$def) {
				return null;
			}
			$width = ($width_override) ? $width_override : $def->getWidth();
			$height = ($height_override) ? $height_override : $def->getHeight();
			$mime = $def->getMediaType();

			try {
				$file = $this->getFile();
				if ((!$file && $this->hasChildren()) || $file->getMediaType()->equals(
						new DMMediaType("application", "vnd.dmbridge.compound-object+xml"))) {
					$file = $this->getChild(0)->getFile();
				}
				
				$class = $def->getClass();
				if (!class_exists($class)) {
					throw new DMClassNotFoundException($class);
				}
				$this->viewer = new $class();

				if (!$this->viewer->isLowBandwidthCompatible()
						&& $file->getSize() > $def->getMaxFileSize()
						&& $def->getMaxFileSize() > 0) {
					$this->viewer = new DMFileLinkViewer();
				}
			} catch (DMFileNotFoundException $e) {
				$this->viewer = new DMNullViewer($this, $mime, $width, $height);
				$this->viewer->setMessage($e->getMessage());
			}

			// compound object PDFs will work only with this viewer.
			if ($this->getCompoundType() == "Document-PDF") {
				$this->viewer = new DMGenericPDFViewer();
			}

			$this->viewer->setObject($this);
			$this->viewer->setMediaType($mime);
			$this->viewer->setWidth($width);
			$this->viewer->setHeight($height);
		}
		return $this->viewer;
	}

	/**
	 * @return string
	 */
	public function getViewerClassName() {
		$def = $this->getCollection()->getViewerDefinitionForMediaType(
				$this->getMediaType());
		if (!$def) {
			$def = $this->getCollection()->getViewerDefinitionForMediaType(
					new DMMediaType("unknown", "unknown"));
		}
		if (!$def) {
			return null;
		}
		return $def->getClass();
	}

	/**
	 * @return int The full width of the DMObject's image, if applicable.
	 * @deprecated Deprecated in version 2.1. Use getImageWidth() instead.
	 */
	public function getWidth() {
		return $this->getImageWidth();
	}

	/**
	 * Returns the object's metadata from CONTENTdm.
	 */
	private function getCdmDOMDocument() {
		if ($this->cdm_domdocument) {
			return $this->cdm_domdocument;
		}
		$xml = null;
		dmGetItemInfo($this->getCollection()->getAlias(), $this->getPtr(), $xml);
		if (!empty($xml)) {
			$this->cdm_domdocument = new DOMDocument("1.0", "utf-8");
			/* dmGetItemInfo() may return non-UTF-8 character data, or
			 * apparently even non-any-character-set data, which loadXML()
			 * will, understandably, not deal with in a friendly way. If it's
			 * invalid UTF-8, we run DMString::clean() on it which may either
			 * strip out invalid characters, or strip out non-ASCII characters,
			 * depending on the PHP version. We shouldn't have to do that.
			 * Bug report filed w/ OCLC Support 2010-03-04 by adolski,
			 * SR#1-642143637.
			 */
			if (!DMString::isUTF8($xml)) {
				$xml = DMString::clean($xml);
			}
			$this->cdm_domdocument->loadXML($xml);
			return $this->cdm_domdocument;
		} else {
			throw new DMUnavailableModelException(
					DMLocalizedString::getString("NONEXISTENT_OBJECT"));
		}
	}

	/**
	 * @todo This is stupid
	 */
	private function getCompoundType() {
		if ($this->compound_type) {
			return $this->compound_type;
		}
		if ($this->isChild()) {
			$this->setChildProperties();
		}

		// get compound object structure
		dmGetCompoundObjectInfo(
			$this->getCollection()->getAlias(),
			$this->isChild() ? $this->getParent()->getPtr() : $this->getPtr(),
			$xml);
		if (!strlen($xml)) {
			return null;
		}

		$dxml = new DOMDocument("1.0", "utf-8");
		$dxml->loadXML($xml);

		$this->compound_type = $dxml->getElementsByTagName("type")->item(0)
				->nodeValue;
		return $this->compound_type;
	}

	/**
	 * Called lazily by getMetadata()
	 */
	private function instantiateFields() {
		$dxml = $this->getCdmDOMDocument();
		// create DMDCElement objects from XML structure
		$skip_fields = array('find', 'dmaccess', 'dmcreated', 'dmmodified',
			'dmoclcno', 'dmrecord');
		$children = $dxml->documentElement->childNodes;
		if (!$children instanceof DOMNodeList) {
			return;
		}
		foreach ($children as $node) {
			if (in_array($node->nodeName, $skip_fields)) {
				continue;
			}
			$f = $this->getCollection()->getField($node->nodeName);
			if ($f instanceof DMDCElement) {
				$m = clone $f;
				$m->setValue((string) $node->nodeValue);
				$this->metadata[] = $m;
				unset($m, $f);
			}
		}
	}

	/**
	 * Called lazily by getImageHeight(), getImageWidth(), and getFilename()
	 */
	private function instantiateFileProperties() {
		$filename = $type = $width = $height = null;
		dmGetImageInfo($this->getCollection()->getAlias(),
			$this->getPtr(), $filename, $type, $width, $height);
		$this->setFilename($filename);
		$this->image_height = abs($height);
		$this->image_width = abs($width);
	}

	/**
	 * Sets the page number and title, and compound object type for child
	 * objects.
	 */
	private function setChildProperties() {
		$xml = "";
		dmGetCollectionParameters($this->getCollection()->getAlias(), $name,
				$path);
		$result = GetParent($this->getCollection()->getAlias(),
				$this->getPtr(), $path);
		dmGetCompoundObjectInfo($this->getCollection()->getAlias(), $result,
				$xml);
		if (!$xml) {
			return;
		}

		// dmGetCompoundObjectInfo() may return invalid UTF-8;
		// see getCdmDOMDocument()
		if (!DMString::isUTF8($xml)) {
			$xml = DMString::clean($xml);
		}
		$dxml = new DOMDocument('1.0', 'utf-8');
		$dxml->loadXML($xml);
		$page = 1;
		$this->compound_type = $dxml->documentElement
				->getElementsByTagName('type')->item(0)->nodeValue;
		foreach ($dxml->documentElement->getElementsByTagName('page') as $p) {
			if ($p->getElementsByTagName('pageptr')->item(0)->nodeValue
					== $this->getPtr()) {
				$this->page = (int) $page;
				$this->title = $p->getElementsByTagName('pagetitle')->item(0)
						->nodeValue;
				break;
			}
			$page++;
		}
	}

}
